using System;
using System.Collections;
using System.Reflection;

using Evaluant.OPath;
using Evaluant.OPath.Expressions;

namespace Evaluant.Uss.ObjectContext.Navigator
{
	public class Navigator
    {

        #region Members

        private ArrayList _Roots = new ArrayList();

        #endregion

        #region Ctor

        /// <summary>
		/// Creates a new <see cref="Navigator"/> instance.
		/// </summary>
		public Navigator()
		{
		}

		/// <summary>
		/// Creates a new <see cref="Navigator"/> instance on a specific object.
		/// </summary>
		/// <param name="root">Root.</param>
        public Navigator(object root) : this(new object[] { root })
        {
            if (root == null)
                throw new ArgumentNullException("root");
        }

		/// <summary>
		/// Creates a new <see cref="Navigator"/> instance on an object set.
		/// </summary>
		/// <param name="root">Root.</param>
		public Navigator(ICollection roots)
		{
			if(roots == null)
				throw new ArgumentNullException("roots");

            if (roots.Count == 0)
                throw new ArgumentException("root cannot be null", "roots");

            _Roots.AddRange(roots);
        }

        #endregion

        #region Properties

        /// <summary>
		/// Gets the root elements.
		/// </summary>
		/// <value>The objects which the navigation will be applied on</value>
		public ArrayList Roots
		{
			get { return _Roots; }
        }

        #endregion

        #region SelectElements

        /// <summary>
		/// Selects the elements based on an opath.
		/// </summary>
		/// <param name="opath">Opath.</param>
		public object[] SelectElements(string opath)
		{
			OPathQuery query = new OPathQuery(opath);
			query.Compile();
			
			if(query.HasErrors)
                throw new OPathException(query);

			return SelectElements(query.Path);
		}

		internal object[] SelectElements(Path path)
		{
			ArrayList _Result = new ArrayList(_Roots);

			foreach(Identifier ident in path.Identifiers)
			{
				// Clones the collection
				ArrayList topLevel = new ArrayList(_Result);
				_Result = new ArrayList();

				foreach(object rootElement in topLevel)
				{
					object childSet;
					if(ident.Value == "this")
					{
						// "this" represents the current node
						childSet = new ArrayList();
						((ArrayList)childSet).Add(rootElement);
					}
					else // Try to get the sub item as an Entity
					{
						Type type = rootElement.GetType();
						PropertyInfo propertyInfo = type.GetProperty(ident.Value, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.GetProperty);
						childSet = propertyInfo.GetValue(rootElement,null);
					}

					if(childSet is ICollection)
					{
						if(((ICollection)childSet).Count > 0)
						{
							foreach(object child in (ICollection)childSet)
							{
								if(ident.Constraint != null) // keep this entity if the constraint is verified
								{
									NavigationVisitor checker = new NavigationVisitor();
									checker.Roots.Clear();
									checker.Roots.Add(child);
									ident.Constraint.Accept(checker);
									object result = checker.NodeValue;
									if(result != null && (result is bool) && (bool)result && !_Result.Contains(child))
										_Result.Add(child);
								}
								else
									if(!_Result.Contains(child))
										_Result.Add(child);
							}
						}
					}
					else
					{
						Type type = rootElement.GetType();
						PropertyInfo propertyInfo = type.GetProperty(ident.Value, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.GetProperty);
						object child = propertyInfo.GetValue(rootElement,null);

						if(child == null) // Go to the next item if the specified identifier was not found
							continue;

                        if (ident.Constraint != null) // keep this entity if the constraint is verified
                        {
                            NavigationVisitor checker = new NavigationVisitor();
                            checker.Roots.Clear();
                            checker.Roots.Add(child);
                            ident.Constraint.Accept(checker);
                            object result = checker.NodeValue;
                            if (result != null && (result is bool) && (bool)result && !_Result.Contains(child))
                                _Result.Add(child);
                        }
                        else
                            if (!_Result.Contains(child))
                                _Result.Add(child);
					}
				}
			}

			return _Result.ToArray();
        }

        #endregion

        #region Evaluate

        /// <summary>
		/// Evaluates an opath expression.
		/// </summary>
		/// <param name="expression">Expression.</param>
		/// <returns></returns>
		public object Evaluate(string expression)
		{
			string opath = String.Format("this[{0}]", expression);

			OPathQuery query = new OPathQuery(opath);
			query.Compile();
			
			if(query.HasErrors)
				throw new OPathException(query);

			return Evaluate(query.Path);
		}

		/// <summary>
		/// Evaluates on opath expression on a specific object.
		/// </summary>
		/// <param name="expression">Expression.</param>
		/// <param name="root">Root.</param>
		/// <returns></returns>
		public object Evaluate(string expression, object root)
		{
			string opath = String.Format("this[{0}]", expression);

			OPathQuery query = new OPathQuery(opath);
			query.Compile();
			
			if(query.HasErrors)
                throw new OPathException(query);

			Roots.Clear();
			Roots.Add(root);

			return Evaluate(query.Path);
		}

		internal object Evaluate(Path path)
		{
			Identifier ident = path.Identifiers[0];

			if(ident.Constraint != null) // keep this entity if the constraint is verified
			{
				NavigationVisitor checker = new NavigationVisitor();
				checker.Roots.Clear();
				checker.Roots.AddRange(_Roots);
				ident.Constraint.Accept(checker);
				if( ((object[])checker.NodeValue).Length > 0)
					return ((object[])checker.NodeValue)[0];
				else
					return null;
			}

			return null;
        }

        #endregion
    }
}
